package com.gitee.slowcreator.http.retry;

import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.ArrayUtil;
import com.gitee.slowcreator.http.config.HttpContextManager;
import com.gitee.slowcreator.http.exception.HttpRetryException;
import com.gitee.slowcreator.http.params.RetryHandleResult;
import lombok.extern.slf4j.Slf4j;

import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;

/**
 * http重试 非线程安全
 *
 * @author zxw
 * @date 2023/1/8 上午8:34
 */
@Slf4j
public class HttpRetry<T> {

    private final Set<Class<? extends Throwable>> noRetryException;

    private final Set<Class<? extends Throwable>> retryException;

    private final boolean traverseCauses;

    private final HttpRetryStrategy httpRetryStrategy;

    private final boolean open;

    private final int retryTimes;

    public HttpRetry(HttpRetryStrategy httpRetryStrategy) {

        if (httpRetryStrategy == null) {
            throw new NullPointerException("初始化HttpRetry失败，策略不能为空！");
        }

        this.httpRetryStrategy = httpRetryStrategy;
        Class<? extends Throwable>[] noRetryFor = httpRetryStrategy.getNoRetryFor();
        if (ArrayUtil.isNotEmpty(noRetryFor)) {
            this.noRetryException = new HashSet<>(Arrays.asList(noRetryFor));
        } else {
            this.noRetryException = new HashSet<>(0);
        }
        Class<? extends Throwable>[] retryFor = httpRetryStrategy.getRetryFor();
        if (ArrayUtil.isNotEmpty(retryFor)) {
            this.retryException = new HashSet<>(Arrays.asList(retryFor));
        } else {
            this.retryException = new HashSet<>(2);
            // 未设置的话重试异常的话 默认HttpRetryException进行重试
            this.retryException.add(HttpRetryException.class);
        }
        this.traverseCauses = httpRetryStrategy.isTraverseCauses();
        this.open = httpRetryStrategy.isOpen();
        this.retryTimes = httpRetryStrategy.getRetryTimes();
    }

    public RetryHandleResult<T> handle(RetryExecutor<T> requestExecutor) {

        int retry = this.retryTimes;
        boolean shouldRetry;
        RetryHandleResult<T> returnData = null;
        int actualExecuteTimes = 0;
        do {
            RetryHandleResult<T> retryHandleResult = null;
            try {
                retryHandleResult = this.retryHandle(requestExecutor);
                returnData = retryHandleResult;
            } catch (Throwable e) {
                log.error("traceId:{},执行请求失败:", HttpContextManager.get().getTraceId(), e);
            }

            if (retryHandleResult == null) {
                shouldRetry = true;
            } else {
                shouldRetry = retryHandleResult.isRetry();
            }
            ++actualExecuteTimes;
        } while (shouldRetry && this.open && retry-- > 0);

        if (returnData == null) {
            returnData = new RetryHandleResult<>(null, false, false);
        }
        // 设置重试次数 第一次正常执行不算
        returnData.setRetryTimes(actualExecuteTimes - 1);

        return returnData;
    }

    /**
     * @param requestExecutor request 执行器
     * @return 是否需要重试 true 需要重试 false 不需要重试
     */
    private RetryHandleResult<T> retryHandle(RetryExecutor<T> requestExecutor) {

        try {
            return requestExecutor.handle(null);
        } catch (Throwable e) {

            if (this.check(e, noRetryException)) {
                // 如果是不需要重试的异常 直接返回了
                log.error("发生不需要重试异常:", e);
                return new RetryHandleResult<>(null, false, false);
            }

            if (this.check(e, retryException)) {
                // 需要重试的异常
                log.error("发生重试异常:", e);
                return new RetryHandleResult<>(null, true, false);
            }
            // 其他情况继续抛出异常
            throw e;
        }
    }

    /**
     * 检查异常 本段代码参考 spring-retry
     *
     * @param throwable  检查的异常
     * @param exceptions 异常列表
     * @return
     */
    private boolean check(Throwable throwable, Set<Class<? extends Throwable>> exceptions) {

        boolean value = this.doCheck(throwable, exceptions);
        // 如果不要遍历异常 直接返回最后一个结果
        if (!traverseCauses) {
            return value;
        }

        if (!value) {
            Throwable cause = throwable;
            do {
                if (exceptions.contains(cause.getClass())) {
                    return true;
                }
                cause = cause.getCause();
                value = this.doCheck(cause, exceptions);
            }
            while (cause != null && !value);
        }

        return value;
    }

    private boolean doCheck(Throwable throwable, Set<Class<? extends Throwable>> exceptions) {

        if (throwable == null || CollectionUtil.isEmpty(exceptions)) {
            return false;
        }

        Class<? extends Throwable> exceptionClass = throwable.getClass();
        if (exceptions.contains(exceptionClass)) {
            return true;
        }

        // check for subclasses
        boolean value = false;
        for (Class<?> cls = exceptionClass; !cls.equals(Object.class) && !value; cls = cls.getSuperclass()) {
            value = exceptions.contains(cls);
        }

        if (value) {
            exceptions.add(exceptionClass);
            return true;
        }

        return false;
    }
}
